iT邦幫忙

1

【Karman.js】建立卡門樹來批次封裝 API - 02

  • 分享至 

  • xImage
  •  

上一章節有提到,Karman 會用 defineAPI(option) 進行單一支 API 的封裝,並返回一個可以基於 option 內的配置發起請求的函式(FinalAPI),而這章節會開始提到如何建立一個可以批次管理多支 API 的方法—— defineKarman

卡門樹 Karman Tree

「卡門樹 Karman Tree」其實顧名思義就是一種樹狀結構,樹上的每個節點會有 0 到 * 個子節點指標,也會存放著該節點的 0 到 * 個 FinalAPI,還會管理著從祖父節點所繼承下來的共同設定。

在建立 Karman Tree 時,需要使用到 defineKarman 這支 API,這支 API 所返回的物件就是一個節點,排除一些共同設定(headers, interceptors, .etc)以外的參數,defineKarman 會有以下幾個可設定的選項,這些參數通常會影響到最後卡門樹的結構或擁有一些特殊的行為:

  • root?: boolean:當前節點是否為根節點。
  • url?: string:當前節點所管理的 URL 或 URL 片段,有特殊的繼承規則。
  • api?: Record<string, FianlAPI>:此節點上所封裝的 APIs,為一個物件,key 是 API 名稱,value 是 defineAPI 的返回值 FinalAPI
  • route?: Record<string, Karman>:子節點們,為一個物件,key 是子節點名稱,value 是 defineKarman 的返回值。

以下先不以實際可發送請求的 API 演示如何建構卡門樹,以及如何訪問節點及節點上的 FinalAPI

import { defineKarman, defineAPI } from "@vic0627/karman"

const karman = defineKarman({
    root: true,
    url: "...",
    api: {
        /**
         * 根節點上的 Final API
         */
        api01: defineAPI({
            // ...
        })
    },
    route: {
        /**
         * 子節點
         */
        node01: defineKarman({
            url: "...",
            api: {
                /**
                 * 子節點上的 Final API
                 */
                node01api01: defineAPI({
                    // ...
                })
            }
        })
    }
})

const [resPromise01] = karman.api01()                // 從根節點訪問根節點上的 Final API
resPromise01.then((res) => console.log(res))

const [resPromise01] = karman.node01.node01api01()   // 從根節點訪問子節點,在從子節點訪問子節點上的 Final API
resPromise01.then((res) => console.log(res))

路徑管理

若還記得上一章所介紹的 defineAPI,會發現它與 defineKarman 一樣都有 url 參數,且一樣都為非必須參數,那麼在 FinalAPI 該怎麼取得請求的 URL 呢?

這部分就必須提到 Karman 的其中的一項特色「路徑管理」,路徑會由父節點往下繼承、拼接,若是 FinalAPI 沒有設定 url,就會參考它所屬節點的 url,若是當前的節點並沒有設定 url,就會參考父層節點的 url,若是以前端最熟悉的 XML 來舉例節點與 FinalAPI 的路徑的繼承關係:

<karman root url="https://fakestoreapi.com" base-url="https://fakestoreapi.com">
    <final-api url="auth/login" base-url="https://fakestoreapi.com/auth/login" />
    <karman url="products" base-url="https://fakestoreapi.com/products">
        <final-api base-url="https://fakestoreapi.com/products" />
        <final-api url=":id" base-url="https://fakestoreapi.com/products/:id" />
        <final-api url="categories" base-url="https://fakestoreapi.com/products/categories" />
        <!-- and more... -->
    </karman>
    <karman url="carts" base-url="https://fakestoreapi.com/carts">
        <final-api url=":id" base-url="https://fakestoreapi.com/carts/:id" />
        <final-api url="../other/path" base-url="https://fakestoreapi.com/other/path" />
        <!-- and more... -->
    </karman>
</karman>

你會在在最後一個 <final-api />url 看到有相對路徑存在,並且 Karman 不但容許其存在,而且是會生效的,這是因為 Karman 在設計之初的目的,就是要能夠給開發人員最大的彈性去管理路由,因為在一些 legacy 的專案中,後端 API 可能不會按照 Restful 風格設計。

若是將上面的 XML 以程式實作,範例如下:

import { defineKarman, defineAPI } from "@vic0627/karman"

export default defineKarman({
    root: true,
    url: "https://fakestoreapi.com",
    api: {
        login: defineAPI({
            url: "auth/login"
        })
    },
    route: {
        product: defineKarman({
            url: "products",
            api: {
                getAll: defineAPI(),
                getById: defineAPI({
                    url: ":id"
                }),
                getCategories: defineAPI({
                    url: "categories"
                })
            }
        }),
        cart: defineKarman({
            url: "carts",
            api: {
                getById: defineAPI({
                    url: ":id"
                }),
                outOfParadigm: defineAPI({
                    url: "../other/path"
                })
            }
        })
    }
})

繼承事件

「繼承事件」是一個會發生在當節點的 root 屬性被設置為 true 時所觸發的事件,事件觸發時,會由根節點開始將可繼承的設定往子節點傳播,若是繼承的設定被子節點給複寫,複寫後的設定將作為新的繼承設定繼續往子孫節點傳播。

卡門樹只能有一個根節點,且必須是頂層的節點,否則 Karman 將拋出錯誤。

FinalAPI 同樣會有繼承,但並不是發生在初始化時,也就是 FinalAPI 的繼承事件不會跟節點的繼承事件同時發生。

比較要注意的事情是,屬性覆寫的行為同樣會發生在物件型別的屬性,這是什麼意思呢?

defineKarman 當中會有兩個可繼承的物件屬性 headersauth,這兩個屬性在繼承時,不是引用物件的址,而是會在子節點上複製一個新的物件,再以子節點的物件進行複寫,因此假設程式碼如下:

// ...

export default defineKarman({
    // ...
    headers: {
        "Content-Type": "application/json",
        Token: "S2FybWFuIGlzIHRoZSBiZXN0IQ=="
    },
    api: {
        api01: defineAPI({
            // ...
            headers: {
                Hello: "Karman"
            }
        })
    },
    route: {
        node01: defineKarman({
            headers: {
                "Content-Type": "text/plain",
            },
            api: {
                node01api01: defineAPI({
                    // ...
                    headers: {
                        Token: "SSBsb3ZlIEthcm1hbiE="
                    }
                })
            }
        })
    }
})

我們可以得到各個節點與 FinalAPI 的 headers 如下:

  • root

    {
        "Content-Type": "application/json",
        "Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ=="
    }
    
  • root.api01

    {
        "Content-Type": "application/json",
        "Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ==",
        "Hello": "Karman"
    }
    
  • root.node01

    {
        "Content-Type": "text/plain",
        "Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ=="
    }
    
  • root.node01.node01api01

    {
        "Content-Type": "text/plain",
        "Token": "SSBsb3ZlIEthcm1hbiE="
    }
    

可繼承的設定

在卡門樹中常見的可繼承的設定還有下列這部分,這邊以功能性進行分類,並在 Karman 特有的功能上簡單說明一下:

  • 一般功能性設定:

    • validation 使否啟用驗證引擎。
  • 快取設定:

    • cache 是否啟用響應快取。
    • cacheExpireTime 快取有效時間。
    • cacheStrategy 快取存放的位置。
  • 請求設定:

    • headers
    • auth
    • timeout
    • timeoutErrorMessage
    • responseType
    • headerMap
    • withCredentials
    • and more...
  • 攔截器:

    • onRequest 請求發起前呼叫,可以對請求物件進行攔截,寫入動態設定等。
    • onResponse 請求完成時呼叫,可定義請求成功時的條件。

根節點

在 Karman 中,會有一些特殊的功能或設定只能透過根節點觸發,這些設定或功能通常也不具備繼承的特性。

排程任務

是的,卡門樹也具備排程任務的機制,但這機制目前只負擔快取的清除任務而已,而排程任務採被動觸發的機制,所以我們只能設定每次執行排程的時間間隔 scheduleInterval

當今天排程管理器接收到任務,會把任務加入一個隊列,當隊列內存在一個以上(含)的任務時,就會自動啟用排程任務機制,並在固定的時間點上執行隊列中所有任務,而當有任務完成時,該任務就會從隊列中剃除,直到隊列為空,排程管理器就會自動關閉。

另外,目前 Karman 只有一個排程管理器,意思就是說,當今天存在兩個以上的卡門樹時,這兩個卡門樹會共用同一個排程管理器,並且只會以第二個卡門樹所設定的 scheduleInterval 為準。

依賴外掛插件

後續的章節裡,會陸陸續續提到 Karman 的其中一個功能 Middleware,這些 Middleware 都是函式型別,並且會綁定當前的節點作為 this 所指向的上下文。

通常在這些 Middleware 內,可能會進行一些基本的資料處理,資料處理的共通邏輯就能以外掛的形式註冊到卡門樹上,再透過 Middleware 內的 this 訪問外掛的共通邏輯或常數等,而註冊必須統一由根節點進行,否則 Karman 將會拋出錯誤。

Karman 本身已經有內建的外掛模組,包含以下兩個:

  • Karman._typeCheck:包含各種型別驗證的方法,為 Karman 驗證引擎所使用的基本模組。
  • Karman._pathResolver:為路徑字串的操作模組,本身也被 Karman 廣泛使用。

而要在卡門樹上註冊外掛需要使用 Karman.$use(plugins) 這個方法,而外怪本身需要有一個 install(karman) 具體方法,假設我們有一個函式 _add() 要註冊為外掛:

// /karman/plugins/add.js
export default function _add(a, b) {
    return a + b
}

// install() 的具體實現
Object.defineProperty(_add, "install", {
    value: (karman) => {
        Object.defineProperty(karman, "_add", {
            value: _add
        })
    }
})

接下來就能在根節點上嘗試註冊此外掛,並且假設根節點上有一個子節點 product

// karman/index.js
import { defineKarman, defineAPI } from "@vic0627/karman"
import product from "./karman/routes/product.js"
import _add from "./plugins/add.js"

const karman = defineKarman({
    // ...
    route: {
        /**
         * ## 商品管理 API
         */
        product
    }
})

// 註冊外掛函式
kaman.$use(_add)

export default karman

後續我們就能在這座卡門樹上的任一 Middleware 用 this 訪問這個外掛:

// karman/routes/product.js
// ...

export default defineKarman({
    // ...
    onRequest() {
        const sum = this._add(2, 4)
        console.log(sum)
    },
    api: {
        getAll: defineAPI({
            // ...
            onSuccess(res) {
                const sum = this._add(9, 8)
                console.log(sum)
            }
        })
    }
})

讓外掛的語法支援自動完成

透過上述方式註冊好外掛後,的確能從 Middleware 中調用到外掛,但美中不足的是,註冊外掛並沒有支援語法提示,因此可以透過外掛的聲明文件,對 Karman 進行模組聲明,擴展 KarmanDependencies 介面:

// /karman/plugins/add.d.ts
interface Add {
    (a: number, b: number): number;
    (a: string, b: string): string;
}

export default function _add: Add;

declare module "@vic0627/karman" {
    interface KarmanDependencies {
        /**
         * 相加
         */
        _add: Add;
    }
}

今天這個章節主要介紹了什麼是卡門樹,以及卡門樹是如何去做路由管理以及卡門樹的各項基本設定,下一個章節會會開始介紹 Karman 的核心功能之一——「參數驗證引擎」。


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言